/*
 * Copyright (C) 2008 Peter Kling
 * 
 * This file is part of 'A Kind of Billiard', a graphical billiard simulation.
 * 
 * 'A Kind of Billiard' is free software: you can redistribute it and/or modify it under the terms of the GNU General
 * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
 * later version.
 * 
 * 'A Kind of Billiard' is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
 * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License along with 'A Kind of Billiard'. If not, see
 * <http://www.gnu.org/licenses/>.
 */

package de.akob.shader;

import java.util.HashMap;

import com.sun.opengl.cg.CGcontext;
import com.sun.opengl.cg.CGparameter;
import com.sun.opengl.cg.CGprogram;
import com.sun.opengl.cg.CgGL;

/**
 * Shader class that allows easy loading, enabling and disabling of shaders.
 */
public class Shader {
	/**
	 * Loads a shader from the given filename using the given profile and context.
	 * 
	 * @return The loaded shader program.
	 * @throws IllegalArgumentException if the given cgContext is null.
	 */
	public static CGprogram loadShader(String filename, int profile, CGcontext cgContext) {
		if (cgContext == null)
			throw new IllegalArgumentException("Need a none null CGcontext for loading shaders.");
		
		// create shader program from given filename
		CGprogram shProg = CgGL.cgCreateProgramFromFile(cgContext, CgGL.CG_SOURCE, filename, profile, null, null);
		if (shProg == null) {
			int err = CgGL.cgGetError();
			System.err.println("Compile shader [" + filename + "] " + CgGL.cgGetErrorString(err));
			if (CgGL.cgGetLastListing(cgContext) != null) {
				System.err.println(CgGL.cgGetLastListing(cgContext) + "\n");
			}
			System.exit(1);
		}
		
		// load shader program
		CgGL.cgGLLoadProgram(shProg);
		int err = CgGL.cgGetError();
		if (err != CgGL.CG_NO_ERROR) {
			System.out.println("Load shader [" + filename + "]: " + CgGL.cgGetErrorString(err));
			System.exit(1);
		}
		
		// return loaded shader program
		return shProg;
	}
	
	private CGcontext            cgContext;
	private int                  cgVertexProfile;
	private int                  cgFragProfile;
	private CGprogram            cgVertexProg         = null;
	private CGprogram            cgFragmentProg       = null;
	private Shader               fallbackShader       = null;
	
	// hash maps for shader parameters
	HashMap<String, CGparameter> cgVertexParameters   = new HashMap<String, CGparameter>();
	HashMap<String, CGparameter> cgFragmentParameters = new HashMap<String, CGparameter>();
	
	/**
	 * Creates a new Shader object with the given files as sources for fragment/vertex programs. If one of the given
	 * filenames is null, the standard fragment/vertex program will be used.
	 */
	public Shader(String vertexSource, String fragmentSource) {
		initCg();
		
		// load vertex shader
		if (vertexSource != null)
			cgVertexProg = loadShader(vertexSource, cgVertexProfile, cgContext);
		else
			cgVertexProg = null;
		
		// load fragment shader
		if (fragmentSource != null)
			cgFragmentProg = loadShader(fragmentSource, cgFragProfile, cgContext);
		else
			cgFragmentProg = null;
	}
	
	/**
	 * Creates a new Shader object with the given files as sources for fragment/vertex programs. If fallbackShader is
	 * not null, the specified shader will be activated as soon as this one is disabled. If none is specified, the
	 * OpenGL standard shaders will be activated upon disabling this shader.
	 */
	public Shader(String vertexSource, String fragmentSource, Shader fallbackShader) {
		this(vertexSource, fragmentSource);
		this.fallbackShader = fallbackShader;
	}
	
	/**
	 * Adds the given parameter to the fragment shaders parameter list. If the parameter is already in the list, nothing
	 * is done.
	 * 
	 * @return true if (and only if) the given parameter is in the parameter list after the call.
	 */
	private boolean addFragmentParameter(String paramName) {
		if (cgFragmentParameters.containsKey(paramName) == false) {
			CGparameter param = CgGL.cgGetNamedParameter(cgFragmentProg, paramName);
			if (param == null) {
				System.err.println("Requested parameter '" + paramName + "' is not a parameter of this shader.");
				return false;
			}
			cgFragmentParameters.put(paramName, param);
		}
		return true;
	}
	
	/**
	 * Adds the given parameter to the vertex shaders parameter list. If the parameter is already in the list, nothing
	 * is done.
	 * 
	 * @return true if (and only if) the given parameter is in the parameter list after the call.
	 */
	private boolean addVertexParameter(String paramName) {
		if (cgVertexParameters.containsKey(paramName) == false) {
			CGparameter param = CgGL.cgGetNamedParameter(cgVertexProg, paramName);
			if (param == null) {
				System.err.println("Requested parameter '" + paramName + "' is not a parameter of this shader.");
				return false;
			}
			cgVertexParameters.put(paramName, param);
		}
		return true;
	}
	
	/**
	 * Disables the shaders bound to this object. This method will activate the fallback shader if one was specified at
	 * construction time or - if none was given - the OpenGL standard shader.
	 */
	public void disable() {
		CgGL.cgGLDisableProfile(cgVertexProfile);
		CgGL.cgGLDisableProfile(cgFragProfile);
		if (fallbackShader != null)
			fallbackShader.enable();
	}
	
	/**
	 * Enables the shaders bound to this object.
	 */
	public void enable() {
		// enable vertex shader if available
		if (cgVertexProg != null) {
			CgGL.cgGLEnableProfile(cgVertexProfile);
			CgGL.cgGLBindProgram(cgVertexProg);
		}
		
		// enable fragment shader if available
		if (cgFragmentProg != null) {
			CgGL.cgGLEnableProfile(cgFragProfile);
			CgGL.cgGLBindProgram(cgFragmentProg);
		}
	}
	
	/**
	 * Initializes Cg.
	 */
	public void initCg() {
		// create Cg context
		cgContext = CgGL.cgCreateContext();
		
		// get vertex profile
		cgVertexProfile = CgGL.cgGLGetLatestProfile(CgGL.CG_GL_VERTEX);
		if (cgVertexProfile == CgGL.CG_PROFILE_UNKNOWN) {
			System.err.println("Invalid vertex profile");
			System.exit(1);
		}
		CgGL.cgGLSetOptimalOptions(cgVertexProfile);
		
		// get fragment profile
		cgFragProfile = CgGL.cgGLGetLatestProfile(CgGL.CG_GL_FRAGMENT);
		if (cgFragProfile == CgGL.CG_PROFILE_UNKNOWN) {
			System.err.println("Invalid fragment profile");
			System.exit(1);
		}
		CgGL.cgGLSetOptimalOptions(cgFragProfile);
	}
	
	/**
	 * Sets the shader parameter indicated by the given string.
	 */
	public void setFragmentParameter(String paramName, double value) {
		if (addFragmentParameter(paramName) == false)
			return;
		CgGL.cgSetParameter1d(cgFragmentParameters.get(paramName), value);
	}
	
	/**
	 * Sets the shader parameter indicated by the given string.
	 */
	public void setFragmentParameter(String paramName, double v1, double v2) {
		if (addFragmentParameter(paramName) == false)
			return;
		CgGL.cgSetParameter2d(cgFragmentParameters.get(paramName), v1, v2);
	}
	
	/**
	 * Sets the shader parameter indicated by the given string.
	 */
	public void setFragmentParameter(String paramName, double v1, double v2, double v3) {
		if (addFragmentParameter(paramName) == false)
			return;
		CgGL.cgSetParameter3d(cgFragmentParameters.get(paramName), v1, v2, v3);
	}
	
	/**
	 * Sets the shader parameter indicated by the given string.
	 */
	public void setFragmentParameter(String paramName, double v1, double v2, double v3, double v4) {
		if (addFragmentParameter(paramName) == false)
			return;
		CgGL.cgSetParameter4d(cgFragmentParameters.get(paramName), v1, v2, v3, v4);
	}
	
	/**
	 * Sets the shader parameter indicated by the given string.
	 */
	public void setFragmentParameter(String paramName, double[] values) {
		if (addFragmentParameter(paramName) == false)
			return;
		if (values.length == 1)
			setFragmentParameter(paramName, values[0]);
		else if (values.length == 2)
			setFragmentParameter(paramName, values[0], values[1]);
		else if (values.length == 3)
			setFragmentParameter(paramName, values[0], values[1], values[2]);
		else if (values.length == 4)
			setFragmentParameter(paramName, values[0], values[1], values[2], values[3]);
		else {
			System.err.println("Got to many arguments for shader parameter (not more than 4 allowed).");
			return;
		}
	}
	
	/**
	 * Sets the shader parameter indicated by the given string.
	 */
	public void setFragmentParameter(String paramName, float value) {
		if (addFragmentParameter(paramName) == false)
			return;
		CgGL.cgSetParameter1f(cgFragmentParameters.get(paramName), value);
	}
	
	/**
	 * Sets the shader parameter indicated by the given string.
	 */
	public void setFragmentParameter(String paramName, float v1, float v2) {
		if (addFragmentParameter(paramName) == false)
			return;
		CgGL.cgSetParameter2f(cgFragmentParameters.get(paramName), v1, v2);
	}
	
	/**
	 * Sets the shader parameter indicated by the given string.
	 */
	public void setFragmentParameter(String paramName, float v1, float v2, float v3) {
		if (addFragmentParameter(paramName) == false)
			return;
		CgGL.cgSetParameter3f(cgFragmentParameters.get(paramName), v1, v2, v3);
	}
	
	/**
	 * Sets the shader parameter indicated by the given string.
	 */
	public void setFragmentParameter(String paramName, float v1, float v2, float v3, float v4) {
		if (addFragmentParameter(paramName) == false)
			return;
		CgGL.cgSetParameter4f(cgFragmentParameters.get(paramName), v1, v2, v3, v4);
	}
	
	/**
	 * Sets the shader parameter indicated by the given string.
	 */
	public void setFragmentParameter(String paramName, float[] values) {
		if (addFragmentParameter(paramName) == false)
			return;
		if (values.length == 1)
			setFragmentParameter(paramName, values[0]);
		else if (values.length == 2)
			setFragmentParameter(paramName, values[0], values[1]);
		else if (values.length == 3)
			setFragmentParameter(paramName, values[0], values[1], values[2]);
		else if (values.length == 4)
			setFragmentParameter(paramName, values[0], values[1], values[2], values[3]);
		else {
			System.err.println("Got to many arguments for shader parameter (not more than 4 allowed).");
			return;
		}
	}
	
	/**
	 * Sets the shader parameter indicated by the given string.
	 */
	public void setFragmentParameter(String paramName, int value) {
		if (addFragmentParameter(paramName) == false)
			return;
		CgGL.cgSetParameter1i(cgFragmentParameters.get(paramName), value);
	}
	
	/**
	 * Sets the shader parameter indicated by the given string.
	 */
	public void setFragmentParameter(String paramName, int v1, int v2) {
		if (addFragmentParameter(paramName) == false)
			return;
		CgGL.cgSetParameter2i(cgFragmentParameters.get(paramName), v1, v2);
	}
	
	/**
	 * Sets the shader parameter indicated by the given string.
	 */
	public void setFragmentParameter(String paramName, int v1, int v2, int v3) {
		if (addFragmentParameter(paramName) == false)
			return;
		CgGL.cgSetParameter3i(cgFragmentParameters.get(paramName), v1, v2, v3);
	}
	
	/**
	 * Sets the shader parameter indicated by the given string.
	 */
	public void setFragmentParameter(String paramName, int v1, int v2, int v3, int v4) {
		if (addFragmentParameter(paramName) == false)
			return;
		CgGL.cgSetParameter4i(cgFragmentParameters.get(paramName), v1, v2, v3, v4);
	}
	
	/**
	 * Sets the shader parameter indicated by the given string.
	 */
	public void setFragmentParameter(String paramName, int[] values) {
		if (addFragmentParameter(paramName) == false)
			return;
		if (values.length == 1)
			setFragmentParameter(paramName, values[0]);
		else if (values.length == 2)
			setFragmentParameter(paramName, values[0], values[1]);
		else if (values.length == 3)
			setFragmentParameter(paramName, values[0], values[1], values[2]);
		else if (values.length == 4)
			setFragmentParameter(paramName, values[0], values[1], values[2], values[3]);
		else {
			System.err.println("Got to many arguments for shader parameter (not more than 4 allowed).");
			return;
		}
	}
	
	/**
	 * Sets the shader parameter indicated by the given string.
	 */
	public void setVertexParameter(String paramName, double value) {
		if (addVertexParameter(paramName) == false)
			return;
		CgGL.cgSetParameter1d(cgVertexParameters.get(paramName), value);
	}
	
	/**
	 * Sets the shader parameter indicated by the given string.
	 */
	public void setVertexParameter(String paramName, double v1, double v2) {
		if (addVertexParameter(paramName) == false)
			return;
		CgGL.cgSetParameter2d(cgVertexParameters.get(paramName), v1, v2);
	}
	
	/**
	 * Sets the shader parameter indicated by the given string.
	 */
	public void setVertexParameter(String paramName, double v1, double v2, double v3) {
		if (addVertexParameter(paramName) == false)
			return;
		CgGL.cgSetParameter3d(cgVertexParameters.get(paramName), v1, v2, v3);
	}
	
	/**
	 * Sets the shader parameter indicated by the given string.
	 */
	public void setVertexParameter(String paramName, double v1, double v2, double v3, double v4) {
		if (addVertexParameter(paramName) == false)
			return;
		CgGL.cgSetParameter4d(cgVertexParameters.get(paramName), v1, v2, v3, v4);
	}
	
	/**
	 * Sets the shader parameter indicated by the given string.
	 */
	public void setVertexParameter(String paramName, double[] values) {
		if (addVertexParameter(paramName) == false)
			return;
		if (values.length == 1)
			setVertexParameter(paramName, values[0]);
		else if (values.length == 2)
			setVertexParameter(paramName, values[0], values[1]);
		else if (values.length == 3)
			setVertexParameter(paramName, values[0], values[1], values[2]);
		else if (values.length == 4)
			setVertexParameter(paramName, values[0], values[1], values[2], values[3]);
		else {
			System.err.println("Got to many arguments for shader parameter (not more than 4 allowed).");
			return;
		}
	}
	
	/**
	 * Sets the shader parameter indicated by the given string.
	 */
	public void setVertexParameter(String paramName, float value) {
		if (addVertexParameter(paramName) == false)
			return;
		CgGL.cgSetParameter1f(cgVertexParameters.get(paramName), value);
	}
	
	/**
	 * Sets the shader parameter indicated by the given string.
	 */
	public void setVertexParameter(String paramName, float v1, float v2) {
		if (addVertexParameter(paramName) == false)
			return;
		CgGL.cgSetParameter2f(cgVertexParameters.get(paramName), v1, v2);
	}
	
	/**
	 * Sets the shader parameter indicated by the given string.
	 */
	public void setVertexParameter(String paramName, float v1, float v2, float v3) {
		if (addVertexParameter(paramName) == false)
			return;
		CgGL.cgSetParameter3f(cgVertexParameters.get(paramName), v1, v2, v3);
	}
	
	/**
	 * Sets the shader parameter indicated by the given string.
	 */
	public void setVertexParameter(String paramName, float v1, float v2, float v3, float v4) {
		if (addVertexParameter(paramName) == false)
			return;
		CgGL.cgSetParameter4f(cgVertexParameters.get(paramName), v1, v2, v3, v4);
	}
	
	/**
	 * Sets the shader parameter indicated by the given string.
	 */
	public void setVertexParameter(String paramName, float[] values) {
		if (addVertexParameter(paramName) == false)
			return;
		if (values.length == 1)
			setVertexParameter(paramName, values[0]);
		else if (values.length == 2)
			setVertexParameter(paramName, values[0], values[1]);
		else if (values.length == 3)
			setVertexParameter(paramName, values[0], values[1], values[2]);
		else if (values.length == 4)
			setVertexParameter(paramName, values[0], values[1], values[2], values[3]);
		else {
			System.err.println("Got to many arguments for shader parameter (not more than 4 allowed).");
			return;
		}
	}
	
	/**
	 * Sets the shader parameter indicated by the given string.
	 */
	public void setVertexParameter(String paramName, int value) {
		if (addVertexParameter(paramName) == false)
			return;
		CgGL.cgSetParameter1i(cgVertexParameters.get(paramName), value);
	}
	
	/**
	 * Sets the shader parameter indicated by the given string.
	 */
	public void setVertexParameter(String paramName, int v1, int v2) {
		if (addVertexParameter(paramName) == false)
			return;
		CgGL.cgSetParameter2i(cgVertexParameters.get(paramName), v1, v2);
	}
	
	/**
	 * Sets the shader parameter indicated by the given string.
	 */
	public void setVertexParameter(String paramName, int v1, int v2, int v3) {
		if (addVertexParameter(paramName) == false)
			return;
		CgGL.cgSetParameter3i(cgVertexParameters.get(paramName), v1, v2, v3);
	}
	
	/**
	 * Sets the shader parameter indicated by the given string.
	 */
	public void setVertexParameter(String paramName, int v1, int v2, int v3, int v4) {
		if (addVertexParameter(paramName) == false)
			return;
		CgGL.cgSetParameter4i(cgVertexParameters.get(paramName), v1, v2, v3, v4);
	}
	
	/**
	 * Sets the shader parameter indicated by the given string.
	 */
	public void setVertexParameter(String paramName, int[] values) {
		if (addVertexParameter(paramName) == false)
			return;
		if (values.length == 1)
			setVertexParameter(paramName, values[0]);
		else if (values.length == 2)
			setVertexParameter(paramName, values[0], values[1]);
		else if (values.length == 3)
			setVertexParameter(paramName, values[0], values[1], values[2]);
		else if (values.length == 4)
			setVertexParameter(paramName, values[0], values[1], values[2], values[3]);
		else {
			System.err.println("Got to many arguments for shader parameter (not more than 4 allowed).");
			return;
		}
	}
}
